package com.aliengen.thingple.thingple.api;

import android.util.Base64;
import android.util.Log;

import com.aliengen.thingple.thingple.api.exception.APICallException;
import com.aliengen.thingple.thingple.api.exception.FieldNotFoundException;
import com.aliengen.thingple.thingple.api.models.Cylinder;
import com.aliengen.thingple.thingple.api.models.Operator;
import com.aliengen.thingple.thingple.tag.BundleTag;
import com.aliengen.thingple.thingple.tag.CylinderTag;
import com.aliengen.thingple.thingple.tag.Tag;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Random;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

/**
 * This class is part of the Thingple mobile application for the CMC168 Handheld reader.
 *
 * Copyright AlienGen Limited 2016. All rights reserved.
 *
 * @author Timothé Mermet-Buffet <tim@aliengen.com>
 */
public class ThingpleAPIConnector {

    protected String url;
    protected String client_id;
    protected String client_secret;

    protected String deviceId;

    public ThingpleAPIConnector()
    {

    }

    public void setURL(String url)
    {
        if(url.length() == 0)
        {
            System.out.println("Invalid URL!");
            return;
        }

        if(url.charAt(url.length()-1) == '/')
        {
            url = url.substring(0, url.length()-1);
        }

        this.url = url;
    }

    public void setCredentials(String client_id, String client_secret)
    {
        this.client_id = client_id;
        this.client_secret = client_secret;
    }

    public void setDeviceId(String deviceId)
    {
        this.deviceId = deviceId;
    }

    private String convertStreamToString(InputStream is)
    {
        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        StringBuilder sb = new StringBuilder();

        String line = null;
        try
        {
            while ((line = reader.readLine()) != null) {
                sb.append(line + "\n");
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try
            {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return sb.toString();
    }

    public static final String md5(final String s) {
        try {
            // Create MD5 Hash
            MessageDigest digest = MessageDigest.getInstance("MD5");
            digest.update(s.getBytes("UTF-8"));
            byte messageDigest[] = digest.digest();

            // Create Hex String
            StringBuilder hexString = new StringBuilder();
            for (byte aMessageDigest : messageDigest) {
                String h = Integer.toHexString(0xFF & aMessageDigest);
                while (h.length() < 2) {
                    h = "0" + h;
                }
                hexString.append(h);
            }
            return hexString.toString();

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch(UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return "";
    }

    public static final String sha256(final String content, final String secret) {
        try {
            Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
            SecretKeySpec secret_key = new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256");
            sha256_HMAC.init(secret_key);

            byte[] messageDigest = sha256_HMAC.doFinal(content.getBytes("UTF-8"));

            // Create Hex String
            StringBuilder hexString = new StringBuilder();
            for (byte aMessageDigest : messageDigest) {
                String h = Integer.toHexString(0xFF & aMessageDigest);
                while (h.length() < 2) {
                    h = "0" + h;
                }
                hexString.append(h);
            }
            return hexString.toString();

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch(InvalidKeyException e)
        {
            e.printStackTrace();
        } catch(UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return "";
    }

    protected void setHeaders(HttpURLConnection urlConnection, String resource, String post) {
        Long utc_timestamp = new Long(System.currentTimeMillis() / 1000);

        String method = "GET";
        Integer size = 0;
        if (post != null && post.length() > 0) {
            method = "POST";
            size = post.getBytes().length;
            urlConnection.setDoOutput(true);
        }

        /**
         * Generate random number
         */
        Random r = new Random();
        Integer nonce = new Integer(r.nextInt(1024));

        /**
         * Generate MD5 and content string
         */
        String md5hash = md5(post);

        String content = method + "\n";
        content += md5hash + "\n";
        content += "application/json\n";
        content += utc_timestamp.toString() + "\n";
        content += nonce.toString() + "\n";
        content += resource + "\n";

        System.out.println("Content: " + content);

        /**
         * Generate HMAC SHA256
         */

        String sha256 = sha256(content, this.client_secret);
        String hmacsha256 = new String(android.util.Base64.encode(sha256.getBytes(), Base64.NO_WRAP));

        System.out.println("HMAC: " + sha256);

        /**
         * Set Headers
         */
        String basicAuth = "Thingple " + this.client_id + ":" + hmacsha256;

        System.out.println("Authorization: " + basicAuth);

        try {
            urlConnection.setRequestMethod(method);
        } catch (Exception e)
        {
            e.printStackTrace();
        }

        urlConnection.setRequestProperty("Authorization", basicAuth);
        urlConnection.setRequestProperty("Content-Type", "application/json");
        urlConnection.setRequestProperty("Content-MD5", md5hash);
        urlConnection.setRequestProperty("Nonce", nonce.toString());
        urlConnection.setRequestProperty("UTC-Timestamp", utc_timestamp.toString());
        urlConnection.setRequestProperty("Content-Length", "" + Integer.toString(size));
        urlConnection.setUseCaches(false);
        urlConnection.setDoInput(true);
    }

    protected JSONObject get(String resource) throws APICallException
    {
        System.out.println("ThingpleAPIConnector: get " + resource);
        InputStream in;
        String result = "";
        try {
            String uri = this.url + resource;

            System.out.println("Connect to " + uri);
            URL hUrl = new URL(uri);

            HttpURLConnection urlConnection = (HttpURLConnection) hUrl.openConnection();

            setHeaders(urlConnection, resource, "");

            in = new BufferedInputStream(urlConnection.getInputStream());

            result = convertStreamToString(in);

            System.out.println("Result: " + result);

            JSONObject json = new JSONObject(result);
            return json.getJSONObject("data");

        } catch(MalformedURLException e)
        {
            throw new APICallException("Invalid URL", e);
        } catch(FileNotFoundException e)
        {
            e.printStackTrace();
            System.out.println(e.getMessage());
            throw new APICallException("IO Error", e);
        }
        catch(IOException e)
        {
            throw new APICallException("IO Error", e);
        } catch(JSONException e)
        {
            throw new APICallException("Invalid JSON response: " + result, e);
        }
    }

    protected JSONObject post(String resource, String data) throws APICallException
    {
        System.out.println("ThingpleAPIConnector: post " + resource);
        InputStream in;
        String result = "";
        try {
            String uri = this.url + resource;

            System.out.println("Connect to " + uri);
            URL hUrl = new URL(uri);

            HttpURLConnection urlConnection = (HttpURLConnection) hUrl.openConnection();

            setHeaders(urlConnection, resource, data);

            DataOutputStream wr = new DataOutputStream(urlConnection.getOutputStream());
            wr.write(data.getBytes());

            in = new BufferedInputStream(urlConnection.getInputStream());

            result = convertStreamToString(in);

            System.out.println("Result: " + result);

            JSONObject json = new JSONObject(result);
            return json.getJSONObject("data");

        } catch(MalformedURLException e)
        {
            throw new APICallException("Invalid URL", e);
        } catch(FileNotFoundException e)
        {
            e.printStackTrace();
            System.out.println(e.getMessage());
            throw new APICallException("IO Error: " + e.toString(), e);
        }
        catch(IOException e)
        {
            throw new APICallException("IO Error: " + e.toString(), e);
        } catch(JSONException e)
        {
            throw new APICallException("Invalid JSON response: " + result, e);
        }
    }

    public Operator getOperator(int operator_id) throws APICallException, FieldNotFoundException
    {
        System.out.println("Get operator " + operator_id + "...");
        JSONObject operator = this.get("/" + operator_id + "/operator/" + this.deviceId);

        System.out.println("Map JSON to object...");
        return new Operator(operator);
    }

    public Cylinder getCylinder(String huashenNumber) throws APICallException, FieldNotFoundException
    {
        JSONObject cylinder = this.get("/" + huashenNumber + "/cylinder");
        return new Cylinder(cylinder);
    }

    public void postCylinder(CylinderTag tag, int operator_id) throws APICallException, FieldNotFoundException
    {
        JSONObject json = new JSONObject();
        try {
            json.accumulate("reader_reference", this.deviceId);
            json.accumulate("operator_id", operator_id);
            json.accumulate("operation_id", tag.getCylinder().operationId);
            json.accumulate("timestamp", tag.getCylinder().lastSeenTimestamp);

            if(tag.getTruck() != null) {
                json.accumulate("truck_id", tag.getTruck().getTruckId());
                json.accumulate("truck_plate_number", tag.getTruck().getPlateNumber());
            }

            String data = json.toString();

            JSONObject d = this.post("/" + tag.getCylinder().huashenNumber + "/cylinder", data);
            tag.getCylinder().update(d);
        } catch(JSONException e)
        {
            Log.e("ThingpleAPIConnector", e.getMessage());
            e.printStackTrace();
        }
    }

    public void postBundle(BundleTag tag, int operator_id) throws APICallException, FieldNotFoundException
    {
        JSONObject json = new JSONObject();
        try {
            json.accumulate("reader_reference", this.deviceId);
            json.accumulate("cylinder_type", "bundle");
            json.accumulate("operator_id", operator_id);
            json.accumulate("operation_id", tag.getOperationId());

            if(tag.getTruck() != null) {
                json.accumulate("truck_id", tag.getTruck().getTruckId());
                json.accumulate("truck_plate_number", tag.getTruck().getPlateNumber());
            }

            String data = json.toString();

            JSONObject d = this.post("/" + tag.getReference() + "/cylinder", data);
            //tag.update(d);
        } catch(JSONException e)
        {
            Log.e("ThingpleAPIConnector", e.getMessage());
            e.printStackTrace();
        }
    }

    public void addAnomaly(CylinderTag tag, int operator_id, String phase_slug, String inspection_slug) throws APICallException
    {
        JSONObject json = new JSONObject();
        try {
            json.accumulate("reader_reference", this.deviceId);
            json.accumulate("operator_id", operator_id);
            json.accumulate("operation_id", tag.getCylinder().operationId);
            json.accumulate("anomaly_phase", phase_slug);
            json.accumulate("anomaly_inspection", inspection_slug);
            json.accumulate("timestamp", tag.getCylinder().lastSeenTimestamp);
            String data = json.toString();

            JSONObject d = this.post("/" + tag.getCylinder().huashenNumber + "/anomaly", data);
            tag.getCylinder().update(d);
        } catch(JSONException e)
        {
            Log.e("ThingpleAPIConnector", e.getMessage());
            e.printStackTrace();
            throw new APICallException("Unable to parse JSON: " + e.getMessage());
        } catch(FieldNotFoundException e)
        {
            Log.e("ThingpleAPIConnector", e.getMessage());
            e.printStackTrace();
            throw new APICallException("Missing field: " + e.getMessage());
        }
    }

    public void addMaintenance(CylinderTag tag, int operator_id, String operation_slug, Boolean maintenance_done) throws APICallException
    {
        JSONObject json = new JSONObject();
        try {
            json.accumulate("reader_reference", this.deviceId);
            json.accumulate("operator_id", operator_id);
            json.accumulate("operation_id", tag.getCylinder().operationId);
            json.accumulate("maintenance_operation", operation_slug);
            json.accumulate("maintenance_done", maintenance_done);
            json.accumulate("timestamp", tag.getCylinder().lastSeenTimestamp);
            String data = json.toString();

            JSONObject d = this.post("/" + tag.getCylinder().huashenNumber + "/maintenance", data);
            tag.getCylinder().update(d);
        } catch(JSONException e)
        {
            Log.e("ThingpleAPIConnector", e.getMessage());
            e.printStackTrace();
            throw new APICallException("Unable to parse JSON: " + e.getMessage());
        } catch(FieldNotFoundException e)
        {
            Log.e("ThingpleAPIConnector", e.getMessage());
            e.printStackTrace();
            throw new APICallException("Missing field: " + e.getMessage());
        }
    }
}
